/**
 * Copyright (c) 2008 Trent Hoeppner
 * 
 * This file is part of BillCalc.
 * 
 * BillCalc is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * BillCalc is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with BillCalc.  If not, see <http://www.gnu.org/licenses/>.
 **/

package com.trentech.billcalc.gui;

import java.awt.Color;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.HashMap;
import java.util.Map;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;

import testcases.MainTest;

import com.trentech.billcalc.event.EventFactory;

/**
 * UserFriendlinessManager provides a central place for managing various
 * "user-friendly" aspects of the GUI, namely tooltips and highlighting of
 * labels to bring the user's attention to a component.  Programmers should 
 * first register any desired components, and when finished, unregister them.
 * 
 * @author Trent Hoeppner
 */
public class UserFriendlinessManager {
    
    /**
     * Manager for tool tip help messages.
     */
    private ToolTipManager toolTipManager;
    
    
    /**
     * The MouseListener used for all components.
     */
    private MouseListener mouseListener = new HoverListener();
    
    
    /**
     * The FocusListener used for all components.
     */
    private FocusListener focusListener = new ActiveListener();
    
    
    /**
     * A mapping from components to states of those components.  For Event 
     * properties, both the component and associated label will be added, but
     * will map to the same EventComponentState.
     */
    private Map<JComponent, ComponentState> componentToStateMap = 
        new HashMap<JComponent, ComponentState>();
    
    
    /**
     * Constructor for UserFriendlinessManager.
     * 
     * @param   frame   the frame to base tool tips off of.  Cannot be null.
     */
    public UserFriendlinessManager(JFrame frame) {
        toolTipManager = new ToolTipManager(frame);
    }
    
    
    /**
     * Registers the given component related to the relevant Event with this.
     * 
     * @param   factory     the EventFactory that contains metadata about the 
     *                      Event.  Cannot be null.
     * @param   component   the component that the Event is linked to.  Cannot 
     *                      be null.
     */
    public void registerEventComponent(EventFactory factory, JComponent component) {
        registerEventComponent(factory, null, component, null);
    }
    
    
    /**
    /**
     * Registers the given component related to the relevant Event property 
     * with this.  This is normally used for an "editor" component and a label
     * that names the property being edited.
     * 
     * @param   factory     the EventFactory that contains metadata about the 
     *                      Event.  Cannot be null.
     * @param   property    the name of the property that is being edited.  
     *                      If null, the component will be registered for the
     *                      whole Event rather than a particular property.
     * @param   component   the component that the Event is linked to.  Cannot 
     *                      be null.
     * @param   label       the label that describes the Event property.  
     *                      Should be null if property is null.
     */
    public void registerEventComponent(EventFactory factory, String property, 
            JComponent component, JLabel label) {
        EventComponentState state = new EventComponentState(factory, component, label, property);
        componentToStateMap.put(component, state);
        
        // original : 
        // if (label != null) {
        // seed fault : 
        if ((label != null) && (MainTest.isOriginal  )) {
        	
            componentToStateMap.put(label, state);
        }
        state.addListeners();
    }
    
    
    /**
     * Registers a tool tip for use with the given component.
     * 
     * @param   component   the component to register.  Cannot be null.
     * @param   toolTipText the text to put in the tooltip when necessary.  
     *                      Cannot be null or empty.
     */
    public void registerToolTip(JComponent component, String toolTipText) {
        ComponentState state = new ComponentState(component, toolTipText);
        componentToStateMap.put(component, state);
        state.addListeners();
    }
    
    
    /**
     * Unbinds the given component that is related to an Event from this.  If
     * a label was registered, only the "editor" component should be 
     * unregistered with this method.
     * 
     * @param   component   the component to unregister.  Cannot be null.
     */
    public void unregisterEventComponent(JComponent component) {
        EventComponentState state = (EventComponentState) componentToStateMap.remove(component);
        if (state.getLabel() != null) {
            componentToStateMap.remove(state.getLabel());
        }
        state.removeListeners();
    }
    
    
    /**
     * Unbinds the given component that has a tool tip from this.
     * 
     * @param   component   the component to unregister.  Cannot be null.
     */
    public void unregisterToolTip(JComponent component) {
        ComponentState state = componentToStateMap.remove(component);
        state.removeListeners();
    }
    
    
    /**
     * Sets the given component as having "changed".  Only useable with 
     * components registered as Events.
     * 
     * @param   component   the component to set as changed.  Cannot be null.
     * @param   changed     true to indicate the Event or property has changed, 
     *                      false otherwise.
     */
    public void setChanged(JComponent component, boolean changed) {
        EventComponentState state = (EventComponentState) componentToStateMap.get(component);
        state.setChanged(changed);
        updateColor(component, state);
    }
    
    
    /**
     * Sets the given component as having an "error".  Only useable with 
     * components registered as Events.
     * 
     * @param   component   the component to set as having an error.  Cannot 
     *                      be null.
     * @param   error       true to indicate the Event or property has an error,
     *                      false otherwise.
     */
    public void setError(JComponent component, boolean error) {
        EventComponentState state = (EventComponentState) componentToStateMap.get(component);
        state.setError(error);
        updateColor(component, state);
    }
    
    
    /**
     * Returns the state object for the given component.  If the component
     * is registered with an Event, it may be the "editor" component or the 
     * label.
     * 
     * @param   component   the component to get the state for.  Cannot be null.
     * 
     * @return  the state that is associated with the component, or null if the 
     *          component was never registered.
     */
    private ComponentState getComponentState(JComponent component) {
        return componentToStateMap.get(component);
    }
    
    
    /**
     * Updates the color for a component based on the state.  If the component
     * is a label, or is registered with an Event that has a label, the Color
     * may change, otherwise this method will have no effect.
     * 
     * @param   component   the component to update the color of.  Cannot be 
     *                      null.
     * @param   state       the state for the component which can return the 
     *                      correct color.  Cannot be null.
     */
    private void updateColor(JComponent component, ComponentState state) {
        if (component instanceof JLabel) {
            component.setForeground(state.getColor());
        }
        
        // change the label for the event property as well
        if (state instanceof EventComponentState) {
            EventComponentState eventState = (EventComponentState) state;
            if (component != eventState.getLabel()) {
                JComponent label = eventState.getLabel();
                if (label != null) {
                    label.setForeground(state.getColor());
                }
            }
        }
    }


    /**
     * ComponentState is used to represent information that is relevant to
     * tooltips for highlighting for a component.
     */
    private class ComponentState {
        
        /**
         * The component whose state is represented.
         */
        private JComponent component;
        
        
        /**
         * The tooltip for the component.
         */
        private String toolTip;
        
        
        /**
         * True if the user has keyboard focus on the component, false 
         * otherwise.
         */
        private boolean focused;
        
        
        /**
         * True if the user's mouse is hovering over the component, false 
         * othwerwise.
         */
        private boolean tempFocused;
        
        
        /**
         * Constructor for ComponentState.
         * 
         * @param   component   the component to represent.  Cannot be null.
         * @param   toolTip     the tooltip for the component.  Cannot be null 
         *                      or empty.
         */
        public ComponentState(JComponent component, String toolTip) {
            this.component = component;
            this.toolTip = toolTip;
        }
        
        
        /**
         * Returns whether the component has keyboard focus.
         * 
         * @return  true if the component has keyboard focus, false otherwise.
         */
        public boolean isFocused() {
            return focused;
        }
        
        
        /**
         * Sets whether the component has keyboard fous.
         * 
         * @param   focused true indicates the component has keyboard focus, 
         *                  false indicates that it does not.
         */
        public void setFocused(boolean focused) {
            this.focused = focused;
        }
        
        
        /**
         * Returns whether the user's mouse is hovering over the component.
         * 
         * @return  true if the user's mouse is hovering over the component, 
         *          false otherwise.
         */
        public boolean isTempFocused() {
            return tempFocused;
        }
        
        
        /**
         * Sets whether the user's mouse is hovering over the component.
         * 
         * @param   tempFocused true indicates that the user's mouse is 
         *                      hovering over the component, false indicates 
         *                      that it is not.
         */
        public void setTempFocused(boolean tempFocused) {
            this.tempFocused = tempFocused;
        }
        
        
        /**
         * Returns the tool tip for the component.
         * 
         * @return  the tool tip for the component.  Will not be null or empty.
         */
        public String getToolTip() {
            return toolTip;
        }
        
        
        /**
         * Returns an appropriate color based on the state of this.
         * 
         * @return  a Color to set for the a label.  Will not be null.
         */
        public Color getColor() {
            Color color;
            if (tempFocused) {
                color = Color.BLUE.darker();
            }
            else {
                color = Color.BLACK;
            }
            
            return color;
        }
        
        
        /**
         * Adds the manager's listeners to the component.
         */
        public void addListeners() {
            component.addMouseListener(mouseListener);
            component.addFocusListener(focusListener);
        }
        
        
        /**
         * Removes the manager's listeners from the component.
         */
        public void removeListeners() {
            component.removeMouseListener(mouseListener);
            component.removeFocusListener(focusListener);
        }
    }
    
    
    /**
     * EventComponentState enhances ComponentState to handle components that
     * have special ties to Events.
     */
    private class EventComponentState extends ComponentState {
        
        /**
         * The label for a given "editor" component.  If this is null, there
         * is no special handling for the main component being an "editor".
         */
        private JLabel label;
        
        
        /**
         * True if the "editor" value changed, false if there was no change.
         */
        private boolean changed;
        
        
        /**
         * True if the "editor" value has an error, false if there is no error.
         */
        private boolean error;
        
        
        /**
         * Consructor for EventComponentState.
         * 
         * @param   factory         the EventFactory for the Event that this is
         *                          tied to.  Cannot be null.
         * @param   component       the component to represent.  Cannot be null.
         * @param   label           the label that works with the "editor".  
         *                          If null, the main component is not treated 
         *                          like an editor.
         * @param   propertyName    the name of the property that is being 
         *                          edited, or null if there is no "editor".
         */
        public EventComponentState(EventFactory factory, JComponent component, 
                JLabel label, String propertyName) {
            
            super(component, propertyName == null 
                    ? factory.getLocalizedDescription() 
                    : factory.getLocalizedDescription(propertyName));
            this.label = label;
        }
        
        
        /**
         * Returns whether the editor value changed.
         * 
         * @return  true if the editor value changed, false otherwise.
         */
        public boolean isChanged() {
            return changed;
        }
        
        
        /**
         * Sets whether the editor value changed.
         * 
         * @param   changed true indicates that the editor value changed, false 
         *                  indicates that it did not change.
         */
        public void setChanged(boolean changed) {
            this.changed = changed;
        }
        
        
        /**
         * Returns whether the editor value has an error.
         * 
         * @return  true if the editor value has an error, false otherwise.
         */
        public boolean isError() {
            return error;
        }
        
        
        /**
         * Sets whether the editor value has an error.
         * 
         * @param   error   true indicates that the editor value has an error,
         *                  false indicates that it does not.
         */
        public void setError(boolean error) {
            this.error = error;
        }
        
        
        /**
         * Returns the label associated with the main "editor" component.
         * 
         * @return  the label for the "editor", or null if there is no such 
         *          label.
         */
        public JLabel getLabel() {
            return label;
        }
        
        
        /**
         * {@inheritDoc}
         */
        public Color getColor() {
            Color color;
            if (error && !isTempFocused()) {
                color = Color.RED;
            }
            else { 
                color = super.getColor();
            }
            
            return color;
        }
        
        
        /**
         * {@inheritDoc}
         */
        public void addListeners() {
            super.addListeners();
            if (label != null) {
                label.addMouseListener(mouseListener);
            }
        }
        
        
        /**
         * {@inheritDoc}
         */
        public void removeListeners() {
            super.removeListeners();
            if (label != null) {
                label.removeMouseListener(mouseListener);
            }
        }
    }
    
    
    /**
     * HoverListener is a mouse listener that when the mouse moves over a 
     * component, shows the tooltip for the component and, if the component is a
     * label (or is an Event property "editor" with an associated label), 
     * changes the color of the label accordingly.
     */
    private class HoverListener extends MouseAdapter {
        
        /**
         * Constructor for HoverListener.
         */
        public HoverListener() {
            // do nothing
        }
        
        
        /**
         * {@inheritDoc}
         */
        public void mouseEntered(MouseEvent e) {
            updateComponents(e, true);
        }


        /**
         * {@inheritDoc}
         */
        public void mouseExited(MouseEvent e) {
            updateComponents(e, false);
        }


        /**
         * Updates the tooltip and color of the relevant component.
         * 
         * @param   e           the event that triggered this.  Cannot be null.
         * @param   tempFocused true if the mouse is hovering over the 
         *                      component, false if it is not.
         */
        private void updateComponents(MouseEvent e, boolean tempFocused) {
            JComponent component = (JComponent) e.getSource();
            ComponentState state = getComponentState(component);
            
            state.setTempFocused(tempFocused);
            
            String tempText = "";
            if (tempFocused) {
                tempText = state.getToolTip();
            }
            
            toolTipManager.setText(tempText);
            
            updateColor(component, state);
        }
    }
    
    
    /**
     * ActiveListener is a focus listener that when a component has keyboard
     * focus, shows the tooltip for the component and, if the component is a
     * label (or is an Event property "editor" with an associated label), 
     * changes the color of the label accordingly.
     */
    private class ActiveListener implements FocusListener {
        
        /**
         * Constructor for ActiveListener.
         */
        public ActiveListener() {
            // do nothing
        }
        
        
        /**
         * {@inheritDoc}
         */
        public void focusGained(FocusEvent e) {
            updateComponents(e, true);
        }


        /**
         * {@inheritDoc}
         */
        public void focusLost(FocusEvent e) {
            updateComponents(e, false);
        }


        /**
         * Updates the tooltip and color of the relevant component.
         * 
         * @param   e       the event that triggered this.  Cannot be null.
         * @param   focused true if the component has keyboard focus, false if 
         *                  it does not.
         */
        private void updateComponents(FocusEvent e, boolean focused) {
            JComponent component = (JComponent) e.getSource();
            ComponentState state = getComponentState(component);
            
            state.setFocused(focused);
            
            String tempText = "";
            if (focused) {
                tempText = state.getToolTip();
            }
            
            toolTipManager.setText(tempText);
            
            updateColor(component, state);
        }
    }
    
}
